item9-绝不在构造和析构过程中调用virtual函数

类构造和析构期间不会发生向下调用

假设你有一套 class 继承体系用来模拟股票交易的类继承体系,例如,购入订单,出售订单等:

//@ 基类
class Transaction {                              
public:
	Transaction();
	virtual void logTransaction() const = 0;
    ...
};

Transaction::Transaction()
{
    ...
	logTransaction();
}

//@ 买家派生类 
class BuyTransaction : public Transaction {    
public:
	virtual void logTransaction() const;
    ...
};

//@ 卖家派生类 
class SellTransaction : public Transaction {    
public:
	virtual void logTransaction() const;
    ...
};

当执行:

BuyTransaction b;

base class 的构造函数先于 derived class 的构造函数执行,而 base class 的构造函数里调用了一个 virtual 函数,此时该虚函数调用的是 base class 版本的,而不是 derived class 版本的,即使目前即将建立的类型是 derived class的。

这样的原因是:由于 base class 的构造函数的执行早于 derived class 构造函数,当 base class 构造函数执行时, derived class 的成员变量尚未初始化。如果此期间调用的 virtual 函数下降至 derived class 阶层,则函数几乎必然会用到 local 成员变量,而那些变量尚未初始化,将会产生未定义行为。

更根本的原因是:在derived class 对象的 base class构造期间,对象的类型是base class 而不是 derived class,不只 virtual 函数会被编译器解析至 base class,若使用运行期类型信息,也会把对象视为 base class类型。对象在 derived class构造函数开始执行前不会成为一个 derived class对象。

相同的道理适用于析构函数。一旦 derived class 析构函数开始执行,对象内的derived class 成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入 base class析构函数后对象成为一个 base class对象,而C++的任何部分包括 virtual 函数、dynamic_casts 等等也就那么看待它。

一般构造/析构函数里使用virtual 函数编译器会发出警告。因为 logTransaction 函数是个纯虚函数,除非被定义(不太有希望,但是可能)否则程序无法连接。

侦测构造函数或析构函数运行期间是否调用 virtual 函数并不是总是这般轻松,当因为存在多个构造函数都执行某些相同的工作时,为避免代码重复而将共同代码(包括调用 virtual函数)放进一个初始化函数 init 内供构造函数调用。

class Transaction {                              
public:
	Transaction(){
        init();
    }
	virtual void logTransaction() const { 
        std::cout << "base class" << std::endl;
    }
private:
	void init(){
        ...
        logTransaction();
    }
};

类中构造函数间接调用了virtual函数,通常不会引发任何编译器和连接器的报错或警告,但实际仍然存在相应问题。

将必要的构造信息向上传递

无法使用 virtual 函数从base class 向下调用,在构造期间,可以通过“令 derived class 将必要的构造信息向上传递至base class构造函数”作为替换和弥补策略。

首先将base class 内的 logTransaction 函数改为 non-virtual,然后要求 derived class 构造函数传递必要的信息给 base class 构造函数,而后 base class构造函数便可安全地调用 non-virtual logTransaction。

class Transaction {
public:
  explicit Transaction(const std::string& logInfo);

  void logTransaction(const std::string& logInfo) const;   // now a non-
                                                           // virtual func
  ...
};

Transaction::Transaction(const std::string& logInfo)
{
  ...
  logTransaction(logInfo);                                 // now a non-
}                                                          // virtual call

class BuyTransaction: public Transaction {
public:
 BuyTransaction(parameters)
 : Transaction(createLogString( parameters ))              // pass log info
  { ... }                                                  // to base class
   ...                                                     // constructor

private:
  static std::string createLogString( parameters );
};

注意private static函数 createLogString 的运用,比起在初始化列表内给予 base class 所需数据,利用辅助构造函数创建一个值传给 base class构造函数往往比较方便(也比较可读)。

总结

  • 在构造或析构期间不要调用虚函数,因为这类调用从不下降至 derived class
  • 不能在基类的构造过程中使用虚函数向下匹配,可以改为让派生类将必要的构造信息上传给基类构造函数作为补偿